// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

// Slim Highlighting for CodeMirror copyright (c) HicknHack Software Gmbh

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(
      require('../../lib/codemirror'),
      require('../htmlmixed/htmlmixed'),
      require('../ruby/ruby'),
    );
  else if (typeof define == 'function' && define.amd)
    // AMD
    define([
      '../../lib/codemirror',
      '../htmlmixed/htmlmixed',
      '../ruby/ruby',
    ], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode(
    'slim',
    function(config) {
      var htmlMode = CodeMirror.getMode(config, { name: 'htmlmixed' });
      var rubyMode = CodeMirror.getMode(config, 'ruby');
      var modes = { html: htmlMode, ruby: rubyMode };
      var embedded = {
        ruby: 'ruby',
        javascript: 'javascript',
        css: 'text/css',
        sass: 'text/x-sass',
        scss: 'text/x-scss',
        less: 'text/x-less',
        styl: 'text/x-styl', // no highlighting so far
        coffee: 'coffeescript',
        asciidoc: 'text/x-asciidoc',
        markdown: 'text/x-markdown',
        textile: 'text/x-textile', // no highlighting so far
        creole: 'text/x-creole', // no highlighting so far
        wiki: 'text/x-wiki', // no highlighting so far
        mediawiki: 'text/x-mediawiki', // no highlighting so far
        rdoc: 'text/x-rdoc', // no highlighting so far
        builder: 'text/x-builder', // no highlighting so far
        nokogiri: 'text/x-nokogiri', // no highlighting so far
        erb: 'application/x-erb',
      };
      var embeddedRegexp = (function(map) {
        var arr = [];
        for (var key in map) arr.push(key);
        return new RegExp('^(' + arr.join('|') + '):');
      })(embedded);

      var styleMap = {
        commentLine: 'comment',
        slimSwitch: 'operator special',
        slimTag: 'tag',
        slimId: 'attribute def',
        slimClass: 'attribute qualifier',
        slimAttribute: 'attribute',
        slimSubmode: 'keyword special',
        closeAttributeTag: null,
        slimDoctype: null,
        lineContinuation: null,
      };
      var closing = {
        '{': '}',
        '[': ']',
        '(': ')',
      };

      var nameStartChar =
        '_a-zA-Z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD';
      var nameChar = nameStartChar + '\\-0-9\xB7\u0300-\u036F\u203F-\u2040';
      var nameRegexp = new RegExp(
        '^[:' + nameStartChar + '](?::[' + nameChar + ']|[' + nameChar + ']*)',
      );
      var attributeNameRegexp = new RegExp(
        '^[:' + nameStartChar + '][:\\.' + nameChar + ']*(?=\\s*=)',
      );
      var wrappedAttributeNameRegexp = new RegExp(
        '^[:' + nameStartChar + '][:\\.' + nameChar + ']*',
      );
      var classNameRegexp = /^\.-?[_a-zA-Z]+[\w\-]*/;
      var classIdRegexp = /^#[_a-zA-Z]+[\w\-]*/;

      function backup(pos, tokenize, style) {
        var restore = function(stream, state) {
          state.tokenize = tokenize;
          if (stream.pos < pos) {
            stream.pos = pos;
            return style;
          }
          return state.tokenize(stream, state);
        };
        return function(stream, state) {
          state.tokenize = restore;
          return tokenize(stream, state);
        };
      }

      function maybeBackup(stream, state, pat, offset, style) {
        var cur = stream.current();
        var idx = cur.search(pat);
        if (idx > -1) {
          state.tokenize = backup(stream.pos, state.tokenize, style);
          stream.backUp(cur.length - idx - offset);
        }
        return style;
      }

      function continueLine(state, column) {
        state.stack = {
          parent: state.stack,
          style: 'continuation',
          indented: column,
          tokenize: state.line,
        };
        state.line = state.tokenize;
      }
      function finishContinue(state) {
        if (state.line == state.tokenize) {
          state.line = state.stack.tokenize;
          state.stack = state.stack.parent;
        }
      }

      function lineContinuable(column, tokenize) {
        return function(stream, state) {
          finishContinue(state);
          if (stream.match(/^\\$/)) {
            continueLine(state, column);
            return 'lineContinuation';
          }
          var style = tokenize(stream, state);
          if (
            stream.eol() &&
            stream.current().match(/(?:^|[^\\])(?:\\\\)*\\$/)
          ) {
            stream.backUp(1);
          }
          return style;
        };
      }
      function commaContinuable(column, tokenize) {
        return function(stream, state) {
          finishContinue(state);
          var style = tokenize(stream, state);
          if (stream.eol() && stream.current().match(/,$/)) {
            continueLine(state, column);
          }
          return style;
        };
      }

      function rubyInQuote(endQuote, tokenize) {
        // TODO: add multi line support
        return function(stream, state) {
          var ch = stream.peek();
          if (ch == endQuote && state.rubyState.tokenize.length == 1) {
            // step out of ruby context as it seems to complete processing all the braces
            stream.next();
            state.tokenize = tokenize;
            return 'closeAttributeTag';
          } else {
            return ruby(stream, state);
          }
        };
      }
      function startRubySplat(tokenize) {
        var rubyState;
        var runSplat = function(stream, state) {
          if (
            state.rubyState.tokenize.length == 1 &&
            !state.rubyState.context.prev
          ) {
            stream.backUp(1);
            if (stream.eatSpace()) {
              state.rubyState = rubyState;
              state.tokenize = tokenize;
              return tokenize(stream, state);
            }
            stream.next();
          }
          return ruby(stream, state);
        };
        return function(stream, state) {
          rubyState = state.rubyState;
          state.rubyState = CodeMirror.startState(rubyMode);
          state.tokenize = runSplat;
          return ruby(stream, state);
        };
      }

      function ruby(stream, state) {
        return rubyMode.token(stream, state.rubyState);
      }

      function htmlLine(stream, state) {
        if (stream.match(/^\\$/)) {
          return 'lineContinuation';
        }
        return html(stream, state);
      }
      function html(stream, state) {
        if (stream.match(/^#\{/)) {
          state.tokenize = rubyInQuote('}', state.tokenize);
          return null;
        }
        return maybeBackup(
          stream,
          state,
          /[^\\]#\{/,
          1,
          htmlMode.token(stream, state.htmlState),
        );
      }

      function startHtmlLine(lastTokenize) {
        return function(stream, state) {
          var style = htmlLine(stream, state);
          if (stream.eol()) state.tokenize = lastTokenize;
          return style;
        };
      }

      function startHtmlMode(stream, state, offset) {
        state.stack = {
          parent: state.stack,
          style: 'html',
          indented: stream.column() + offset, // pipe + space
          tokenize: state.line,
        };
        state.line = state.tokenize = html;
        return null;
      }

      function comment(stream, state) {
        stream.skipToEnd();
        return state.stack.style;
      }

      function commentMode(stream, state) {
        state.stack = {
          parent: state.stack,
          style: 'comment',
          indented: state.indented + 1,
          tokenize: state.line,
        };
        state.line = comment;
        return comment(stream, state);
      }

      function attributeWrapper(stream, state) {
        if (stream.eat(state.stack.endQuote)) {
          state.line = state.stack.line;
          state.tokenize = state.stack.tokenize;
          state.stack = state.stack.parent;
          return null;
        }
        if (stream.match(wrappedAttributeNameRegexp)) {
          state.tokenize = attributeWrapperAssign;
          return 'slimAttribute';
        }
        stream.next();
        return null;
      }
      function attributeWrapperAssign(stream, state) {
        if (stream.match(/^==?/)) {
          state.tokenize = attributeWrapperValue;
          return null;
        }
        return attributeWrapper(stream, state);
      }
      function attributeWrapperValue(stream, state) {
        var ch = stream.peek();
        if (ch == '"' || ch == "'") {
          state.tokenize = readQuoted(
            ch,
            'string',
            true,
            false,
            attributeWrapper,
          );
          stream.next();
          return state.tokenize(stream, state);
        }
        if (ch == '[') {
          return startRubySplat(attributeWrapper)(stream, state);
        }
        if (stream.match(/^(true|false|nil)\b/)) {
          state.tokenize = attributeWrapper;
          return 'keyword';
        }
        return startRubySplat(attributeWrapper)(stream, state);
      }

      function startAttributeWrapperMode(state, endQuote, tokenize) {
        state.stack = {
          parent: state.stack,
          style: 'wrapper',
          indented: state.indented + 1,
          tokenize: tokenize,
          line: state.line,
          endQuote: endQuote,
        };
        state.line = state.tokenize = attributeWrapper;
        return null;
      }

      function sub(stream, state) {
        if (stream.match(/^#\{/)) {
          state.tokenize = rubyInQuote('}', state.tokenize);
          return null;
        }
        var subStream = new CodeMirror.StringStream(
          stream.string.slice(state.stack.indented),
          stream.tabSize,
        );
        subStream.pos = stream.pos - state.stack.indented;
        subStream.start = stream.start - state.stack.indented;
        subStream.lastColumnPos = stream.lastColumnPos - state.stack.indented;
        subStream.lastColumnValue =
          stream.lastColumnValue - state.stack.indented;
        var style = state.subMode.token(subStream, state.subState);
        stream.pos = subStream.pos + state.stack.indented;
        return style;
      }
      function firstSub(stream, state) {
        state.stack.indented = stream.column();
        state.line = state.tokenize = sub;
        return state.tokenize(stream, state);
      }

      function createMode(mode) {
        var query = embedded[mode];
        var spec = CodeMirror.mimeModes[query];
        if (spec) {
          return CodeMirror.getMode(config, spec);
        }
        var factory = CodeMirror.modes[query];
        if (factory) {
          return factory(config, { name: query });
        }
        return CodeMirror.getMode(config, 'null');
      }

      function getMode(mode) {
        if (!modes.hasOwnProperty(mode)) {
          return (modes[mode] = createMode(mode));
        }
        return modes[mode];
      }

      function startSubMode(mode, state) {
        var subMode = getMode(mode);
        var subState = CodeMirror.startState(subMode);

        state.subMode = subMode;
        state.subState = subState;

        state.stack = {
          parent: state.stack,
          style: 'sub',
          indented: state.indented + 1,
          tokenize: state.line,
        };
        state.line = state.tokenize = firstSub;
        return 'slimSubmode';
      }

      function doctypeLine(stream, _state) {
        stream.skipToEnd();
        return 'slimDoctype';
      }

      function startLine(stream, state) {
        var ch = stream.peek();
        if (ch == '<') {
          return (state.tokenize = startHtmlLine(state.tokenize))(
            stream,
            state,
          );
        }
        if (stream.match(/^[|']/)) {
          return startHtmlMode(stream, state, 1);
        }
        if (stream.match(/^\/(!|\[\w+])?/)) {
          return commentMode(stream, state);
        }
        if (stream.match(/^(-|==?[<>]?)/)) {
          state.tokenize = lineContinuable(
            stream.column(),
            commaContinuable(stream.column(), ruby),
          );
          return 'slimSwitch';
        }
        if (stream.match(/^doctype\b/)) {
          state.tokenize = doctypeLine;
          return 'keyword';
        }

        var m = stream.match(embeddedRegexp);
        if (m) {
          return startSubMode(m[1], state);
        }

        return slimTag(stream, state);
      }

      function slim(stream, state) {
        if (state.startOfLine) {
          return startLine(stream, state);
        }
        return slimTag(stream, state);
      }

      function slimTag(stream, state) {
        if (stream.eat('*')) {
          state.tokenize = startRubySplat(slimTagExtras);
          return null;
        }
        if (stream.match(nameRegexp)) {
          state.tokenize = slimTagExtras;
          return 'slimTag';
        }
        return slimClass(stream, state);
      }
      function slimTagExtras(stream, state) {
        if (stream.match(/^(<>?|><?)/)) {
          state.tokenize = slimClass;
          return null;
        }
        return slimClass(stream, state);
      }
      function slimClass(stream, state) {
        if (stream.match(classIdRegexp)) {
          state.tokenize = slimClass;
          return 'slimId';
        }
        if (stream.match(classNameRegexp)) {
          state.tokenize = slimClass;
          return 'slimClass';
        }
        return slimAttribute(stream, state);
      }
      function slimAttribute(stream, state) {
        if (stream.match(/^([\[\{\(])/)) {
          return startAttributeWrapperMode(
            state,
            closing[RegExp.$1],
            slimAttribute,
          );
        }
        if (stream.match(attributeNameRegexp)) {
          state.tokenize = slimAttributeAssign;
          return 'slimAttribute';
        }
        if (stream.peek() == '*') {
          stream.next();
          state.tokenize = startRubySplat(slimContent);
          return null;
        }
        return slimContent(stream, state);
      }
      function slimAttributeAssign(stream, state) {
        if (stream.match(/^==?/)) {
          state.tokenize = slimAttributeValue;
          return null;
        }
        // should never happen, because of forward lookup
        return slimAttribute(stream, state);
      }

      function slimAttributeValue(stream, state) {
        var ch = stream.peek();
        if (ch == '"' || ch == "'") {
          state.tokenize = readQuoted(ch, 'string', true, false, slimAttribute);
          stream.next();
          return state.tokenize(stream, state);
        }
        if (ch == '[') {
          return startRubySplat(slimAttribute)(stream, state);
        }
        if (ch == ':') {
          return startRubySplat(slimAttributeSymbols)(stream, state);
        }
        if (stream.match(/^(true|false|nil)\b/)) {
          state.tokenize = slimAttribute;
          return 'keyword';
        }
        return startRubySplat(slimAttribute)(stream, state);
      }
      function slimAttributeSymbols(stream, state) {
        stream.backUp(1);
        if (stream.match(/^[^\s],(?=:)/)) {
          state.tokenize = startRubySplat(slimAttributeSymbols);
          return null;
        }
        stream.next();
        return slimAttribute(stream, state);
      }
      function readQuoted(quote, style, embed, unescaped, nextTokenize) {
        return function(stream, state) {
          finishContinue(state);
          var fresh = stream.current().length == 0;
          if (stream.match(/^\\$/, fresh)) {
            if (!fresh) return style;
            continueLine(state, state.indented);
            return 'lineContinuation';
          }
          if (stream.match(/^#\{/, fresh)) {
            if (!fresh) return style;
            state.tokenize = rubyInQuote('}', state.tokenize);
            return null;
          }
          var escaped = false,
            ch;
          while ((ch = stream.next()) != null) {
            if (ch == quote && (unescaped || !escaped)) {
              state.tokenize = nextTokenize;
              break;
            }
            if (embed && ch == '#' && !escaped) {
              if (stream.eat('{')) {
                stream.backUp(2);
                break;
              }
            }
            escaped = !escaped && ch == '\\';
          }
          if (stream.eol() && escaped) {
            stream.backUp(1);
          }
          return style;
        };
      }
      function slimContent(stream, state) {
        if (stream.match(/^==?/)) {
          state.tokenize = ruby;
          return 'slimSwitch';
        }
        if (stream.match(/^\/$/)) {
          // tag close hint
          state.tokenize = slim;
          return null;
        }
        if (stream.match(/^:/)) {
          // inline tag
          state.tokenize = slimTag;
          return 'slimSwitch';
        }
        startHtmlMode(stream, state, 0);
        return state.tokenize(stream, state);
      }

      var mode = {
        // default to html mode
        startState: function() {
          var htmlState = CodeMirror.startState(htmlMode);
          var rubyState = CodeMirror.startState(rubyMode);
          return {
            htmlState: htmlState,
            rubyState: rubyState,
            stack: null,
            last: null,
            tokenize: slim,
            line: slim,
            indented: 0,
          };
        },

        copyState: function(state) {
          return {
            htmlState: CodeMirror.copyState(htmlMode, state.htmlState),
            rubyState: CodeMirror.copyState(rubyMode, state.rubyState),
            subMode: state.subMode,
            subState:
              state.subMode &&
              CodeMirror.copyState(state.subMode, state.subState),
            stack: state.stack,
            last: state.last,
            tokenize: state.tokenize,
            line: state.line,
          };
        },

        token: function(stream, state) {
          if (stream.sol()) {
            state.indented = stream.indentation();
            state.startOfLine = true;
            state.tokenize = state.line;
            while (
              state.stack &&
              state.stack.indented > state.indented &&
              state.last != 'slimSubmode'
            ) {
              state.line = state.tokenize = state.stack.tokenize;
              state.stack = state.stack.parent;
              state.subMode = null;
              state.subState = null;
            }
          }
          if (stream.eatSpace()) return null;
          var style = state.tokenize(stream, state);
          state.startOfLine = false;
          if (style) state.last = style;
          return styleMap.hasOwnProperty(style) ? styleMap[style] : style;
        },

        blankLine: function(state) {
          if (state.subMode && state.subMode.blankLine) {
            return state.subMode.blankLine(state.subState);
          }
        },

        innerMode: function(state) {
          if (state.subMode)
            return { state: state.subState, mode: state.subMode };
          return { state: state, mode: mode };
        },

        //indent: function(state) {
        //  return state.indented;
        //}
      };
      return mode;
    },
    'htmlmixed',
    'ruby',
  );

  CodeMirror.defineMIME('text/x-slim', 'slim');
  CodeMirror.defineMIME('application/x-slim', 'slim');
});
